在本系列文中,所有的程式碼以及測試都可以在 should-i-use-fp-ts 找到,今日的範例放在 src/day-09
並且有習題和測試可以讓大家練習。
延續昨天的話題,昨天學會使用 O.map
讓普通的函式 (f: A => B)
可以維持在 Option
的規則下面運行。
但如果在 O.map
中使用有關判斷的函式 (f: A => Option<B>)
,會導致 Option
有嵌套的情況。
以下是昨天的例子,需求是做到第三步的時候將不及格的分數過濾出來視為 None
,但 O.map
本身又會在函式回傳後再套上一層 Option
。
type IsFailed = (x: number) => O.Option<number>;
const isFailed: IsFailed = x => x > 60 ? O.some(x) : O.none;
const adjustScoreFailed: AdjustScore = flow(
O.of, // Option<number>
O.map(x => x * 1.2), // Option<number>
O.map(isFailed), // Option<Option<number>>
O.map(Math.round), // never: type mismatch
O.map(x => x > 100 ? 100 : x), // never
);
這裡需要實作一個 flatten
函式,來將嵌套的 Option
整平。
/** fltten :: Option (Option a) -> Option a */
type Flatten = <A>(x: O.Option<O.Option<A>>) => O.Option<A>;
const flatten: Flatten = x => x._tag === 'None' ? O.none : x.value;
flatten
可以接受所有嵌套的 Option
: Option<Option<A>>
,並將嵌套減少一層,如此一來就可以使用在 adjustScore
之中。
const adjustScoreFlatten: AdjustScore = flow( // use 40 as an example
O.of, // { _tag: 'Some', value: 40 }
O.map(x => x * 1.2), // { _tag: 'Some', value: 48 }
O.map(isFailed), // { _tag: 'None' }
O.flatten, // { _tag: 'None' }
O.map(Math.round), // { _tag: 'None' }
O.map(x => x > 100 ? 100 : x), // { _tag: 'None' }
);
但如果有需要複數個判斷函式 (f: A => Option<B>)
,這樣寫就會造成充滿了 O.map
, O.flatten
的組合,但 O.flatten
並沒有承載任何的業務邏輯,這樣會形成閱讀程式碼的斷點,可以觀看以下範例。
// example
pipe(
xs, // [1, 2, 3, 4, 5]
head, // { _tag: 'Some', value: 1 }
O.map(inverse), // Option<{ _tag: 'Some', value: 1 }>
O.flatten, // { _tag: 'Some', value: 1 }
O.map(inverse), // Option<{ _tag: 'Some', value: 1 }>
O.flatten, // { _tag: 'Some', value: 1 }
O.map(inverse), // Option<{ _tag: 'Some', value: 1 }>
O.flatten, // { _tag: 'Some', value: 1 }
)
這邊需要再設計一個 flatMap
函式來將 O.map + O.flatten
的繁複操作組合起來,進一步簡化程式碼。
/** flatMap :: (a -> Option b) -> Option a -> Option b */
export type FlatMap = <A, B>(f: (a: A) => O.Option<B>) =>
(x: O.Option<A>) => O.Option<B>;
export const flatMap: FlatMap = f => x => pipe(x, O.map(f), O.flatten);
// equals
export const flatMap: FlatMap = f => x =>
x._tag === 'None' ? O.none : f(x.value);
O.flatMap
接收判斷函式 (f: A => Option<B>)
以及 (x: Option<A>)
,然後直接輸出經過 O.map, O.flatten
處理的函數。
const adjustScore: AdjustScore = flow( // use 40 as an example
O.of, // { _tag: 'Some', value: 40 }
O.map(x => x * 1.2), // { _tag: 'Some', value: 48 }
O.flatMap(isFailed), // { _tag: 'None' }
O.map(Math.round), // { _tag: 'None' }
O.map(x => x > 100 ? 100 : x), // { _tag: 'None' }
);
今天的主題在 should-i-use-fp-ts src/day-09
有習題和測試可以練習,大家可以嘗試自己能不能根據 Option
和 O.map
的定義寫出自己的 flatten
和 flatMap
。